/*Package route file-handle

上传 -- file-upload
下载 -- file-download
列举文件 -- file-list

TODO: 记录到 数据库

*/
package route

import (
	"crypto/md5"
	"encoding/json"
	"fmt"
	"github.com/gomodule/redigo/redis"
	gouuid "github.com/satori/go.uuid"
	"io"
	"math"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	mysqlMod "route/mysql"
	redisMod "route/redis"
	"strings"
	"time"
)

var (
	ErrNormalTokenNotFound = fmt.Errorf("缺少验证 token")
	ErrNormalTokenExpire   = fmt.Errorf("token 过期")
	ErrNormalTokenUsed     = fmt.Errorf("token 已用")
)

func init() {
	if _, err := os.Stat(FileUploadDir); err != nil {
		if err := os.MkdirAll(FileUploadDir, ModeDir); err != nil {
			panic(err.Error())
		}
	}

	if _, err := os.Stat(FileUploadDirTmp); err != nil {
		if err = os.MkdirAll(FileUploadDirTmp, ModeDir); err != nil {
			panic(err.Error())
		}
	}
}

// PreFileUpload 生成 token
func PreFileUpload(w http.ResponseWriter, r *http.Request) {
	conn := redisMod.Get()
	defer conn.Close()
	uuid, err := gouuid.NewV1()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	if err := conn.Send("SET", FileUploadTokenHeader+uuid.String(), false, "EX", NormalTokenSETExpire); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	w.Header().Set(FileUploadTokenHeader, uuid.String())
}

// FileUpload 实际的(单)文件上传
// TODO: 把文件记录到 mysql
func FileUpload(w http.ResponseWriter, r *http.Request) {
	uuid := w.Header().Get(FileUploadTokenHeader)
	if uuid == "" {
		// 无 token
		http.Error(w, ErrNormalTokenNotFound.Error(), http.StatusUnauthorized)
		return
	}
	conn := redisMod.Get()
	defer conn.Close()
	reply, err := redis.Bool(conn.Do("GET", FileUploadTokenHeader+uuid))
	if err != nil {
		// 过期
		http.Error(w, ErrNormalTokenExpire.Error(), http.StatusUnauthorized)
		return
	} else if reply {
		// 已用
		http.Error(w, ErrNormalTokenUsed.Error(), http.StatusCreated)
		return
	}

	if err = r.ParseMultipartForm(32 << 20); err != nil { // OR `r.ParseForm()`
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// 获得上传文件的信息
	file, header, err := r.FormFile("upload")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer file.Close()
	filename := url.QueryEscape(header.Filename) // 防止 中文乱码 以及 非法字符

	// 创建目的文件
	tmpFilename := FileUploadDirTmp + string(os.PathSeparator) + filename
	f, err := os.Create(tmpFilename)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// copy
	if _, err = io.Copy(f, file); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// 设置 token 已过期
	conn.Send("SET", FileUploadTokenHeader+uuid, true, "EX", NormalTokenSETExpire)

	// 以下部分可能导致文件越大越慢, 因此选择通过一个独立协程来处理
	// [文件 hash 计算 / 实际文件([hash[:5]]/[文件名].[hash])存储 / 数据库记录]
	// go func(filename string, f *os.File) {

	// 添加 与文件内容相关的 hash , 并附加在文件名之后

	md5Sum := countFileMD5(f) // 获取文件 md5 值
	f.Close()
	simpleFilename := filename + "." + md5Sum
	dbFilename := string(os.PathSeparator) + strings.Join([]string{md5Sum[:6], simpleFilename}, string(os.PathSeparator))
	realFilename := strings.Join([]string{FileUploadDir, md5Sum[:6], simpleFilename}, string(os.PathSeparator))
	// mkdir
	if err = os.MkdirAll(FileUploadDir+string(os.PathSeparator)+md5Sum[:6], ModeDir); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	// rename
	if err = os.Rename(tmpFilename, realFilename); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// *r = *r.WithContext(context.WithValue(r.Context(), TestFile, realFilename)) // DEBUG
	// TODO: 记录到 数据库
	// _, ok := r.Context().Value(UserCtx).(int32)
	// if !ok {
	// 	http.Error(w, fmt.Errorf("没有用户").Error(), http.StatusInternalServerError)
	// 	return
	// }
	now := time.Now()
	_, err = mysqlMod.Exec("INSERT INTO process_drawing (path, simple_path, date_create, date_modify) VALUES (?, ?, ?, ?)",
		dbFilename, filename, now.Unix(), now.Unix())
	// _, err = CustomStmts.GetStmt("insert", "process_drawing", 0).Exec(id, id, dbFilename, filename)
	if err != nil {
		http.Error(w, "mysql insert file 失败$"+err.Error(), http.StatusInternalServerError)
		return
	}
	w.Header().Set("upload", dbFilename)
	// }(filename, f)
}

// 取得 md5 值
func countFileMD5(file *os.File) string {
	fileInfo, _ := file.Stat()
	fileSize := fileInfo.Size()

	blocks := uint64(math.Ceil(float64(fileSize) / float64(FileUploadChunk)))
	hash := md5.New()

	for i := uint64(0); i < blocks; i++ {
		blockSize := int(math.Min(float64(FileUploadChunk), float64(fileSize-int64(i*FileUploadChunk))))
		buf := make([]byte, blockSize)

		file.Read(buf)
		io.WriteString(hash, string(buf))
	}

	return fmt.Sprintf("%x", hash.Sum(nil)) // copy
}

type fileListReply struct {
	Complete string `json:"complete,omitempty"`
	Simple   string `json:"simple,omitempty"`
}

// FileList 列举现有文件
// TODO: 从 数据库 取得文件名等信息
func FileList(w http.ResponseWriter, r *http.Request) {
	file, err := os.Open(FileUploadDir)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer file.Close()

	// 读 mysql-process_drawing
	rows, err := mysqlMod.Query("SELECT path, simple_path FROM process_drawing")
	// rows, err := CustomStmts.GetStmt("select", "process_drawing", 0).Query()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	reply := []fileListReply{} // 文件列表
	for rows.Next() {
		simple := fileListReply{}
		rows.Scan(&simple.Complete, &simple.Simple)
		reply = append(reply, simple)
	}

	enc, err := json.Marshal(reply)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	w.Write(enc)
}

// FileDownload 文件下载
func FileDownload(w http.ResponseWriter, r *http.Request) {
	// filename := url.QueryEscape(mux.Vars(r)["file"])
	dbfilename := r.Header.Get("download")
	rs, err := mysqlMod.Query("SELECT id FROM process_drawing WHERE path=?", dbfilename)
	// rs, err := CustomStmts.GetStmt("select", "process_drawing", 1).Query(dbfilename)
	if err != nil {
		http.Error(w, err.Error(), http.StatusNotFound)
		return
	}

	if !rs.Next() {
		http.Error(w, "not fount", http.StatusNotFound)
		return
	}

	file, err := os.Open(FileUploadDir + dbfilename)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer file.Close()

	w.Header().Set("Content-Type", "application/octet-stream")
	w.Header().Set("Content-Disposition", `attachment; filename="`+filepath.Base(dbfilename)+`"`)

	if _, err := io.Copy(w, file); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}
